﻿#region License
// (c) Intergen.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using MediaWiki.Office.Model.Utilities;
using MediaWiki.Common;

namespace MediaWiki.Office.Model
{
  public class ParagraphProperties : ContentProperties
  {
    private Style _definingStyle;

    private List<TabStop> _tabStops = new List<TabStop>();
    private bool? _adjustRightIndentation;
    private bool? _bidi;
    private bool? _contextualSpacing;
    private HorizontalAlignment? _alignment;
    private bool? _keepLinesOnOnePage;
    private bool? _pageBreakBefore;
    private string _styleId;

    private Unit? _indentLeft;
    private Unit? _indentRight;
    private Unit? _indentFirstLine;
    private Unit? _indentHanging;
    private Unit? _spacingBefore;
    private Unit? _spacingAfter;
    private Unit? _spacingLine;
    private LineSpacingRule? _spacingLineRule;

    private bool? _mirrorIndents;
    private bool? _keepWithNextParagraph;

    private int? _numberingLevelNumber;
    private int? _numberingId;

    private Border _borderTop;
    private Border _borderLeft;
    private Border _borderBottom;
    private Border _borderRight;
    private Border _borderBetween;

    private Shading _shading;
    private RunProperties _runProperties;

    private SectionProperties _sectionProperties;
    private ConditionalTableStyleType? _conditionalFormatting;

    public Style DefaultStyle
    {
      get { return DocumentContent.GetStyle(Content, "Normal"); }
    }

    public RunProperties RunProperties
    {
      get { return _runProperties; }
      set { _runProperties = value; }
    }

    public Shading Shading
    {
      get { return _shading; }
      set { _shading = value; }
    }

    public ConditionalTableStyleType? ConditionalFormatting
    {
      get { return _conditionalFormatting; }
      set { _conditionalFormatting = value; }
    }

    public List<TabStop> TabStops
    {
      get
      {
        List<TabStop> tabStops = AppendValues<List<TabStop>>(p => p._tabStops).Select(t => (TabStop)t).ToList();

        // indenting creates an implicit tab stop. primarly used by lists
        if (IndentHanging != null && IndentLeft != null && IndentLeft.Value.Value != 0)
        {
          Unit position = Unit.Twip(IndentLeft.Value.Value);

          tabStops.Add(new TabStop() { Leader = string.Empty, Type = TabStopType.Left, Position = position });
        }

        return tabStops.OrderBy(t => t.Position.Value).ToList();
      }
      set { _tabStops = value; }
    }

    public int? NumberingLevelNumber
    {
      get
      {
        int? levelNumber = GetValue(p => p._numberingLevelNumber, false);
        if (levelNumber != null)
          return levelNumber;

        // blarg
        // http://msdn.microsoft.com/en-us/library/ee922775%28office.14%29.aspx
        NumberingDefinition numberingDefinition = Numbering;
        if (_styleId != null && numberingDefinition != null)
        {
          NumberingLevel numberingLevel = numberingDefinition.NumberingLevels.SingleOrDefault(l => l.StyleId == _styleId);
          if (numberingLevel != null)
            return numberingLevel.Level;
        }

        return null;
      }
      set { _numberingLevelNumber = value; }
    }

    public int? NumberingId
    {
      get { return _numberingId; }
      set { _numberingId = value; }
    }

    public NumberingDefinition Numbering
    {
      get
      {
        int? numberingId = GetValue(p => p._numberingId, false);
        if (numberingId == null
          // TODO: numberingId is zero in some cases. No matching num style...
          || numberingId == 0)
          return null;

        return Content.MainDocument.NumberingDefinitions[numberingId.Value];
      }
    }

    public NumberingLevel NumberingLevel
    {
      get
      {
        if (Numbering == null)
          return null;
        if (NumberingLevelNumber == null)
          return null;

        return Numbering.NumberingLevels[NumberingLevelNumber.Value];
      }
    }

    public string StyleId
    {
      get { return _styleId; }
      set { _styleId = value; }
    }

    public Style Style
    {
      // paragraphs have a default style of "Normal"
      get { return DocumentContent.GetStyle(Content, _styleId) ?? DefaultStyle; }
    }

    public SectionProperties SectionProperties
    {
      get { return _sectionProperties; }
      set { _sectionProperties = value; }
    }

    public Border BorderTop
    {
      get { return _borderTop; }
      set { _borderTop = value; }
    }

    public Border BorderLeft
    {
      get { return _borderLeft; }
      set { _borderLeft = value; }
    }

    public Border BorderBottom
    {
      get { return _borderBottom; }
      set { _borderBottom = value; }
    }

    public Border BorderRight
    {
      get { return _borderRight; }
      set { _borderRight = value; }
    }

    public Border BorderBetween
    {
      get { return _borderBetween; }
      set { _borderBetween = value; }
    }

    public bool? PageBreakBefore
    {
      get { return GetValue(p => p._pageBreakBefore); }
      set { _pageBreakBefore = value; }
    }

    public bool? KeepLinesOnOnePage
    {
      get { return GetValue(p => p._keepLinesOnOnePage); }
      set { _keepLinesOnOnePage = value; }
    }

    public HorizontalAlignment? Alignment
    {
      get { return GetValue(p => p._alignment); }
      set { _alignment = value; }
    }

    public bool? ContextualSpacing
    {
      get { return GetValue(p => p._contextualSpacing); }
      set { _contextualSpacing = value; }
    }

    public bool? Bidi
    {
      get { return GetValue(p => p._bidi); }
      set { _bidi = value; }
    }

    public bool? AdjustRightIndentation
    {
      get { return GetValue(p => p._adjustRightIndentation); }
      set { _adjustRightIndentation = value; }
    }

    public Unit? IndentLeft
    {
      get { return GetValue(p => p._indentLeft); }
      set { _indentLeft = value; }
    }

    public Unit? IndentRight
    {
      get { return GetValue(p => p._indentRight); }
      set { _indentRight = value; }
    }

    public Unit? IndentFirstLine
    {
      get { return GetValue(p => p._indentFirstLine); }
      set { _indentFirstLine = value; }
    }

    public Unit? IndentHanging
    {
      get { return GetValue(p => p._indentHanging); }
      set { _indentHanging = value; }
    }

    public Unit? SpacingBefore
    {
      get { return GetValue(p => p._spacingBefore); }
      set { _spacingBefore = value; }
    }

    public Unit? SpacingAfter
    {
      get { return GetValue(p => p._spacingAfter); }
      set { _spacingAfter = value; }
    }

    public Unit? SpacingLine
    {
      get { return GetValue(p => p._spacingLine); }
      set { _spacingLine = value; }
    }

    public LineSpacingRule? SpacingLineRule
    {
      get { return GetValue(p => p._spacingLineRule); }
      set { _spacingLineRule = value; }
    }

    public bool? MirrorIndents
    {
      get { return GetValue(p => p._mirrorIndents); }
      set { _mirrorIndents = value; }
    }

    public bool? KeepWithNextParagraph
    {
      get { return GetValue(p => p._keepWithNextParagraph); }
      set { _keepWithNextParagraph = value; }
    }

    public ParagraphProperties(Style definingStyle, XElement element)
      : this(definingStyle.ParentContent, element)
    {
      if (definingStyle == null)
        throw new ArgumentNullException("definingStyle");

      _definingStyle = definingStyle;
    }

    public ParagraphProperties(Content parentContent, XElement element)
      : base(parentContent)
    {
      if (element == null)
      {
        // HACK: Avoid null objects when there is not properties element
        // need to rethink this
        _borderBetween = Border.Create(this, null, null);
        _borderBottom = Border.Create(this, null, null);
        _borderLeft = Border.Create(this, null, null);
        _borderRight = Border.Create(this, null, null);
        _borderTop = Border.Create(this, null, null);
        _runProperties = new RunProperties(Content, null);
        _shading = new Shading(this, null);
        return;
      }

      XNamespace ns = element.Name.Namespace;

      _runProperties = new RunProperties(Content, element.Element(ns + "rPr"));

      _adjustRightIndentation = XmlUtils.GetBoolean(element.Element(ns + "adjustRightInd"));
      _bidi = XmlUtils.GetBoolean(element.Element(ns + "bidi"));
      _contextualSpacing = XmlUtils.GetBoolean(element.Element(ns + "contextualSpacing"));
      _alignment = XmlUtils.GetValue<HorizontalAlignment>(element.Element(ns + "jc"));
      _keepLinesOnOnePage = XmlUtils.GetBoolean(element.Element(ns + "keepLines"));
      _pageBreakBefore = XmlUtils.GetBoolean(element.Element(ns + "pageBreakBefore"));

      _indentLeft = Unit.Twip(XmlUtils.GetValue<int>(element.Element(ns + "ind"), "left"));
      _indentRight = Unit.Twip(XmlUtils.GetValue<int>(element.Element(ns + "ind"), "right"));
      _indentFirstLine = Unit.Twip(XmlUtils.GetValue<int>(element.Element(ns + "ind"), "firstLine"));
      _indentHanging = Unit.Twip(XmlUtils.GetValue<int>(element.Element(ns + "ind"), "hanging"));
      _spacingBefore = Unit.Twip(XmlUtils.GetValue<int>(element.Element(ns + "spacing"), "before"));
      _spacingAfter = Unit.Twip(XmlUtils.GetValue<int>(element.Element(ns + "spacing"), "after"));

      _spacingLineRule = XmlUtils.GetValue<LineSpacingRule>(element.Element(ns + "spacing"), "lineRule");
      int? lineSpacingValue = XmlUtils.GetValue<int>(element.Element(ns + "spacing"), "line");
      if (lineSpacingValue != null)
      {
        switch (_spacingLineRule ?? LineSpacingRule.Auto)
        {
          case LineSpacingRule.AtLeast:
          case LineSpacingRule.Exact:
            _spacingLine = Unit.Twip(lineSpacingValue.Value);
            break;
          case LineSpacingRule.Auto:
            _spacingLine = Unit.Percent(lineSpacingValue.Value / (double)240);
            break;
        }
      }

      _mirrorIndents = XmlUtils.GetBoolean(element.Element(ns + "mirrorIndents"));
      _keepWithNextParagraph = XmlUtils.GetBoolean(element.Element(ns + "keepNext"));
      _styleId = XmlUtils.GetValue(element.Element(ns + "pStyle"));

      string conditionalValue = XmlUtils.GetValue(element.Element(ns + "cnfStyle"));
      if (conditionalValue != null)
        _conditionalFormatting = (ConditionalTableStyleType)BitUtils.ConvertToInt32(conditionalValue.Reverse());

      XElement numberingProperties = element.Element(ns + "numPr");
      if (numberingProperties != null)
      {
        _numberingLevelNumber = XmlUtils.GetValue<int>(numberingProperties.Element(ns + "ilvl"));
        _numberingId = XmlUtils.GetValue<int>(numberingProperties.Element(ns + "numId"));
      }

      _sectionProperties = SectionProperties.Create(parentContent, element.Element(ns + "sectPr"));

      _shading = new Shading(this,
       element.Element(ns + "shd"),
       g => GetValue<object>(p => g(p._shading)));
      
      _borderTop = Border.Create(this,
        element.Elements(ns + "pBdr").Elements(ns + "top").SingleOrDefault(),
        g => GetValue<object>(p => g(p._borderTop)));

      _borderLeft = Border.Create(this,
        element.Elements(ns + "pBdr").Elements(ns + "left").SingleOrDefault(),
        g => GetValue<object>(p => g(p._borderLeft)));

      _borderBottom = Border.Create(this,
        element.Elements(ns + "pBdr").Elements(ns + "bottom").SingleOrDefault(),
        g => GetValue<object>(p => g(p._borderBottom)));

      _borderRight = Border.Create(this,
        element.Elements(ns + "pBdr").Elements(ns + "right").SingleOrDefault(),
        g => GetValue<object>(p => g(p._borderRight)));
      
      _borderBetween = Border.Create(this,
        element.Elements(ns + "pBdr").Elements(ns + "between").SingleOrDefault(),
        g => GetValue<object>(p => g(p._borderBetween)));

      var tabStops = from e in element.Elements(ns + "tabs").Elements(ns + "tab")
                     select new TabStop(e);

      _tabStops.AddRange(tabStops);
    }

    public bool BordersEqual(ParagraphProperties paragraphProperties)
    {
      if (paragraphProperties == null)
        throw new ArgumentNullException("paragraph");

      if (!Border.Equals(_borderTop, paragraphProperties.BorderTop))
        return false;
      if (!Border.Equals(_borderBottom, paragraphProperties.BorderBottom))
        return false;
      if (!Border.Equals(_borderLeft, paragraphProperties.BorderLeft))
        return false;
      if (!Border.Equals(_borderRight, paragraphProperties.BorderRight))
        return false;
      if (!Border.Equals(_borderBetween, paragraphProperties.BorderBetween))
        return false;

      return true;
    }

    private void AddStyleAttempts<T>(Style style, Func<ParagraphProperties, T> valueGetter, List<Func<T>> attempts)
    {
      if (style == null)
        return;

      attempts.Add(() => valueGetter(style.ParagraphProperties));
      if (style.BasedOnStyle != null)
        attempts.Add(() => valueGetter(style.BasedOnStyle.ParagraphProperties));
    }

    public List<object> AppendValues<T>(Func<ParagraphProperties, T> valueGetter)
    {
      if (valueGetter == null)
        throw new ArgumentNullException("valueGetter");

      Content parent = Content;

      List<Func<T>> attempts = new List<Func<T>>();

      attempts.Add(() => valueGetter(this));

      if (parent is Paragraph)
      {
        Paragraph paragraph = (Paragraph)parent;
        Table parentTable = Content.GetParentTable(paragraph);

        // numbering level
        if (NumberingLevel != null)
          attempts.Add(() => valueGetter(NumberingLevel.ParagraphProperties));

        AddStyleAttempts(Style, valueGetter, attempts);

        if (parentTable != null && parentTable.Properties.Style != null)
        {
          Style parentTableStyle = parentTable.Properties.Style;
          TableCell parentCell = Content.GetParentCell(paragraph);
          TableRow parentRow = Content.GetParentRow(paragraph);

          // paragraph table conditional style
          if (paragraph.Properties.ConditionalFormatting != null)
            attempts.Add(() => ConditionalTableStyle.GetConditionalTableStyleValue(valueGetter,
              s => s.ParagraphProperties,
              paragraph.Properties.ConditionalFormatting.Value,
              parentTableStyle.ConditionalTableStyles));

          // cell table conditional style
          if (parentCell.Properties.ConditionalFormatting != null)
            attempts.Add(() => ConditionalTableStyle.GetConditionalTableStyleValue(valueGetter,
              s => s.ParagraphProperties,
              parentCell.Properties.ConditionalFormatting.Value,
              parentTableStyle.ConditionalTableStyles));

          // row table conditional style
          if (parentRow.Properties.ConditionalFormatting != null)
            attempts.Add(() => ConditionalTableStyle.GetConditionalTableStyleValue(valueGetter,
              s => s.ParagraphProperties,
              parentRow.Properties.ConditionalFormatting.Value,
              parentTableStyle.ConditionalTableStyles));

          // table style
          AddStyleAttempts(parentTableStyle, valueGetter, attempts);
        }

        // document defaults
        if (parent.MainDocument.DefaultParagraphProperties != null)
          attempts.Add(() => valueGetter(parent.MainDocument.DefaultParagraphProperties));
      }

      return MiscellaneousUtils.AppendValues(attempts);
    }

    public T GetValue<T>(Func<ParagraphProperties, T> valueGetter)
    {
      return GetValue(valueGetter, true);
    }

    public T GetValue<T>(Func<ParagraphProperties, T> valueGetter, bool checkNumberingLevel)
    {
      if (valueGetter == null)
        throw new ArgumentNullException("valueGetter");

      Content parent = Content;

      List<Func<T>> attempts = new List<Func<T>>();

      attempts.Add(() => valueGetter(this));

      if (parent is Paragraph)
      {
        Paragraph paragraph = (Paragraph)parent;
        Table parentTable = Content.GetParentTable(paragraph);

        // numbering level
        if (checkNumberingLevel && NumberingLevel != null)
          attempts.Add(() => valueGetter(NumberingLevel.ParagraphProperties));

        AddStyleAttempts(Style ?? DefaultStyle, valueGetter, attempts);

        if (parentTable != null && parentTable.Properties.Style != null)
        {
          Style parentTableStyle = parentTable.Properties.Style;
          TableCell parentCell = Content.GetParentCell(paragraph);
          TableRow parentRow = Content.GetParentRow(paragraph);

          // paragraph table conditional style
          if (paragraph.Properties.ConditionalFormatting != null)
            attempts.Add(() => ConditionalTableStyle.GetConditionalTableStyleValue(valueGetter,
              s => s.ParagraphProperties,
              paragraph.Properties.ConditionalFormatting.Value,
              parentTableStyle.ConditionalTableStyles));

          // cell table conditional style
          if (parentCell.Properties.ConditionalFormatting != null)
            attempts.Add(() => ConditionalTableStyle.GetConditionalTableStyleValue(valueGetter,
              s => s.ParagraphProperties,
              parentCell.Properties.ConditionalFormatting.Value,
              parentTableStyle.ConditionalTableStyles));

          // row table conditional style
          if (parentRow.Properties.ConditionalFormatting != null)
            attempts.Add(() => ConditionalTableStyle.GetConditionalTableStyleValue(valueGetter,
              s => s.ParagraphProperties,
              parentRow.Properties.ConditionalFormatting.Value,
              parentTableStyle.ConditionalTableStyles));

          AddStyleAttempts(parentTableStyle, valueGetter, attempts);
        }

        // document defaults
        if (parent.MainDocument.DefaultParagraphProperties != null)
          attempts.Add(() => valueGetter(parent.MainDocument.DefaultParagraphProperties));
      }

      return MiscellaneousUtils.EvaluateUntilValue(attempts);
    }
  }
}